/**
 *
 */
package org.jprobe.junit;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;

import org.jprobe.JavaProjectInterface;
import org.jprobe.classpath.AbstractClassPath;
import org.jprobe.codegen.BeanGetterValueInterface;
import org.jprobe.codegen.ClassVisitor;
import org.jprobe.codegen.ClassVisitorInterface;
import org.jprobe.codegen.CodeGenException;
import org.jprobe.codegen.CodeGenerator;
import org.jprobe.codegen.CodeGeneratorInterface;
import org.jprobe.instantiate.Instantiator;

/**
 * JUnitTestCaseGeneratorInterface implementation
 * @author Hussain Al-Mutawa
 * @version 2.0
 * @since 1.0
 */
public class JUnitTestCaseGenerator implements JUnitTestCaseGeneratorInterface {
	//	/** classpath of the project **/
	//	private transient static final AbstractClassPath CLASSPATH=ClassPathReader.getClassPath();
	/** code generator **/
	private transient static final CodeGeneratorInterface CG = new CodeGenerator();
	/** the project hosting the bean **/
	private transient JavaProjectInterface project;
	/** the class to be tested **/
	private transient Class<?>clazz;
	/** junit test blocks **/
	private transient final SortedSet<JUnitTestUnitBlock<Method>>
	privateMethodsJUnitTestUnitBlocks,
	methodsJUnitTestUnitBlocks;
	private transient final SortedSet<JUnitTestUnitBlock<Constructor<?>>>
	constructorsJUnitTestUnitBlocks;
	/** the generated test case file name **/
	private transient String testFileName;
	/**
	 * initializes a new junit test generator specifying the project hosting the test case
	 * @param project a java project
	 */
	public JUnitTestCaseGenerator(final JavaProjectInterface project){
		super();
		this.project=project;
		this.privateMethodsJUnitTestUnitBlocks=new TreeSet<JUnitTestUnitBlock<Method>>();
		this.methodsJUnitTestUnitBlocks=new TreeSet<JUnitTestUnitBlock<Method>>();
		this.constructorsJUnitTestUnitBlocks=new TreeSet<JUnitTestUnitBlock<Constructor<?>>>();
	}



	/**
	 * initializes a new junit test generator
	 * @param project a java project
	 * @param clazz the class for which test cases are generated
	 */
	public JUnitTestCaseGenerator(JavaProjectInterface project,final Class<?>clazz) {
		this(project);
		this.clazz=clazz;
	}

	@Override
	public JUnitTestCaseGeneratorInterface generate() throws Exception {
		resetCollections();
		testFileName = this.clazz.getPackage().getName();
		while(testFileName.contains(".")){testFileName=testFileName.replace('.', File.separatorChar);}
		testFileName = this.project.getJprobeSrcFolder().getCanonicalPath()+File.separator+testFileName+File.separator;
		testFileName += this.clazz.getSimpleName();
		testFileName += "JProbeTestCase.java";
		System.out.println("######## Generating test cases for: "+testFileName+" ##############");
		final ClassVisitorInterface bii = new ClassVisitor();
		bii.setBean(getTargetClass());
		bii.inspect();
		System.out.println(" ------- the bean ["+getTargetClass()+"] has been inspected");
		//test getters first
		for(final Method method:bii.getGettersMethods()){
			final JUnitTestUnitBlock<Method> mjtub = new JUnitTestUnitBlock<Method>(method);
			final BeanGetterValueInterface jutub = bii.get(method);
			final String testingBody = getAssertion(method, jutub );
			mjtub.setExpectedExceptionClass(jutub.getExceptionThrown());
			mjtub.setTestingBody(testingBody);
			this.methodsJUnitTestUnitBlocks.add(mjtub);
			System.out.println(" + Method tested : "+method);
		}
		//test setters (public)
		for(final Method method:bii.getVoidMethods()){
			if(Modifier.isPublic(method.getModifiers())){
				final JUnitTestUnitBlock<Method> mjtub = new JUnitTestUnitBlock<Method>(method);
				final StringBuffer testingBody = new  StringBuffer();
				final String params=CG.getParametersDeclaration(method.getParameterTypes());
				testingBody.append("\t\t"+ AbstractClassPath.getClassBinaryName(getTargetClass()) + " obj = " + new CodeGenerator().getImplementation(getTargetClass()).getCode() + ";\n");
				testingBody.append("\t\t/*void*/ obj."+method.getName() +'('+ params +')' +";\n");
				try{
					method.invoke(bii.getBeanInstance(), new Instantiator().getParametersValues(method.getParameterTypes()));
				}catch(final Throwable t){
					testingBody.append("\t\t/*\n");
					testingBody.append("\t\t * Exception thrown while generating test case : "+ t.getMessage() +"\n");
					testingBody.append("\t\t * EXCEPTION : "+ t.getClass().getName() +"\n");
					testingBody.append("\t\t */\n");
					mjtub.setExpectedExceptionClass(t.getClass().getName());
				}
				testingBody.append(getAllGettersAssertions(bii.getBean(), bii.getBeanInstance()));
				mjtub.setTestingBody(testingBody.toString());
				this.methodsJUnitTestUnitBlocks.add(mjtub);
				System.out.println(" + Method tested : "+method);
			}
		}
		//test constructors, same way as constructors


		return this;
	}

	private transient static final List<Class<?>>PRIMITIVES=Arrays.asList(new Class<?>[]{
			int.class	,Integer.class,
			double.class,Double.class,
			float.class	,Float.class,
			char.class	,Character.class,
			long.class	,Long.class,
			short.class	,Short.class,
			byte.class	,Byte.class,
			boolean.class,Boolean.class,
			String.class
	});
	private transient static final Map<Class<?>,String>FLOATS=new HashMap<Class<?>,String>();
		//Arrays.asList(new Class<?>[]{double.class,Double.class,Float.class,float.class});
	static{
		FLOATS.put(double.class, "Double");
		FLOATS.put(Double.class, "Double");
		FLOATS.put(float.class, "Float");
		FLOATS.put(Float.class, "Float");
	}


	@Override
	public String getAssertion(final Method method,final BeanGetterValueInterface actualValue) throws Exception{
		final Object actual = actualValue.getValue();
		final StringBuffer sbuffer = new StringBuffer();
		final Class<?>type=method.getReturnType();
		final String valueType = AbstractClassPath.getClassBinaryName(type);
		final String clsName = AbstractClassPath.getClassBinaryName(getTargetClass());
		sbuffer.append("\t\t"+clsName + " obj = " + new CodeGenerator().getImplementation(getTargetClass()).getCode() + ";\n");
		final String params=CG.getParametersDeclaration(method.getParameterTypes());

		if(Modifier.isPublic(method.getModifiers())){
			sbuffer.append("\t\t"+valueType+" expected = obj."+method.getName() +'('+ params +')' +";\n");
		}else{
			try{
				boolean bDeclaredMethods = method.getDeclaringClass().equals(getTargetClass());
				//boolean bMethods = method.getDeclaringClass().getMethod(method.getName(), method.getParameterTypes())==method;
				String types="",declared="";
				if(bDeclaredMethods){
					declared="Declared";
				}
				for(final Class<?>tp:method.getParameterTypes()){
					types += CG.getBinaryName(tp) +".class,";
				}
				if(types.endsWith(",")){
					types=types.substring(0,types.length()-1);
				}
				sbuffer.append("\t\tObject expected = null;\n");
				//sbuffer.append("\t\ttry{\n");
				sbuffer.append("\t\tjava.lang.reflect.Method method = "+method.getDeclaringClass().getName()+".class.get"+declared+"Method(\""+method.getName()+"\",new Class<?>[]{"+types+"});\n");
				sbuffer.append("\t\tmethod.setAccessible(true);\n");
				sbuffer.append("\t\texpected = method.invoke(obj,new Object[]{"+ params +"});\n");
				//sbuffer.append("\t\t}catch(Throwable t){\n\t\t\tfail(t.getMessage());\n\t\t}\n");

			}catch(final Throwable t){
				sbuffer.append("\t\t/*\n");
				sbuffer.append("\t\t * Exception thrown while generating test case : "+ t.getMessage() +"\n");
				sbuffer.append("\t\t * EXCEPTION : "+ t.getClass().getName() +"\n");
				sbuffer.append("\t\t */\n");
				sbuffer.append(valueType);
				sbuffer.append("\t\tObject expected = null;\n");
			}
		}
		//final Class<?>type = actual.getClass();

		if(actual==null){
			sbuffer.append("\t\tassertNotNull(expected);\n");
		}else if(PRIMITIVES.contains(type)){
			if(type==String.class){
				sbuffer.append("\t\t"+valueType +" actual = \"" + actual.toString().replaceAll("\\r\\n", "\\\\r\\\\n").replaceAll("\\r", "\\\\r").replaceAll("\\n", "\\\\n") + "\";\n");
			}else if(type==char.class ^ type==Character.class){
				sbuffer.append("\t\t"+valueType +" actual = '" + actual + "';\n");
			}else if(type==long.class || type==Long.class){
				sbuffer.append("\t\t"+valueType +" actual = " + actual + "L;\n");
//			}else if(type==float.class || type==Float.class || type==double.class || type==Double.class){
//				sbuffer.append("\t\t"+valueType +" actual = " + actual + "d;\n");
			}else{
				sbuffer.append("\t\t"+valueType +" actual = ("+ valueType + ')' + actual + ";\n");
			}
			if(FLOATS.keySet().contains(type)){
				sbuffer.append("\t\tassertEquals(("+FLOATS.get(type)+")expected,actual,0.1);\n");
			}else{
				sbuffer.append("\t\tassertEquals(expected,actual);\n");
			}
		}else if(java.util.Collection.class.isAssignableFrom(type) ){
			sbuffer.append("\t\tString actual = \"" + Arrays.toString(((Collection<?>)actual).toArray()) + "\";\n");
			sbuffer.append("\t\tassertEquals(Arrays.toString(expected.toArray()),actual);\n");
		}else if(type.isArray()){
			sbuffer.append("\t\tString actual = \"" + Arrays.toString(array(actual)) + "\";\n");
			sbuffer.append("\t\tassertEquals(Arrays.toString(("+ CG.getBinaryName(actual.getClass()) +")expected),actual);\n");
		}else{

			//type.getDeclaredMethod("toString", new Class<?>[]{});
			sbuffer.append("\t\tString actual = \"" + actual.toString().replaceAll("\\r\\n", "\\\\r\\\\n").replaceAll("\\r", "\\\\r").replaceAll("\\n", "\\\\n") + "\";\n");
			sbuffer.append("\t\tassertEquals(expected.toString(),actual);\n");

			//sbuffer.append(getAllGettersAssertions(type, actual));

		}
		return sbuffer.toString().replaceAll("\\(double\\)NaN\\;", "Double.NaN;")
		.replaceAll("\\(double\\)\\-Infinity", "Double.NEGATIVE_INFINITY")
		.replaceAll("\\(double\\)Infinity", "Double.POSITIVE_INFINITY")
		;
	}

	@Override
	public String getAllGettersAssertions(final Class<?> bean, final Object obj) throws Exception{
		final StringBuffer sbuffer = new StringBuffer();
		final ClassVisitor bi = new ClassVisitor();
		bi.setBean(bean);
		bi.setBeanInstance(obj);

		bi.inspect();	//this instruction was missing which caused the inspector
						//to return no methods at all

		final Method[]mthds=bi.getPublicMethods();
		sbuffer.append("\t\tString actualv,expectedv;\n");
		for(final Method mthd:mthds){
			//if(mthd.isAccessible()){
				final Object val = bi.get(mthd).getValue();
				if(val==null){
					sbuffer.append("\t\texpectedv = \"\"+obj." + mthd.getName() +'('+ CG.getParametersDeclaration(mthd.getParameterTypes()) + ");\n");
					sbuffer.append("\t\tactualv = null;\n" );					
				}else if(val.getClass().isArray()){
					sbuffer.append("\t\texpectedv = deepToString(obj." + mthd.getName() +'('+ CG.getParametersDeclaration(mthd.getParameterTypes()) + "));\n");
					sbuffer.append("\t\tactualv = \"" + deepToString(val) + "\";\n" );
				}else{
					sbuffer.append("\t\texpectedv = \"\"+obj." + mthd.getName() +'('+ CG.getParametersDeclaration(mthd.getParameterTypes()) + ");\n");
					sbuffer.append("\t\tactualv = \"" + val.toString().replaceAll("\\r\\n", "\\\\r\\\\n").replaceAll("\\r", "\\\\r").replaceAll("\\n", "\\\\n") + "\";\n" );
				}
				sbuffer.append("\t\tassertEquals(expectedv,actualv);\n");
			//}
		}
		return sbuffer.toString();
	}

	private String deepToString(final Object array){
		if(!array.getClass().isArray()){
			throw new CodeGenException(array+" is not an array");
		}
		final List<Object> list = new ArrayList<Object>();
		for(int i=0;i<Array.getLength(array);i++){
			list.add( Array.get(array, i) );
		}
		return Arrays.deepToString(list.toArray()).replaceAll("\\r\\n", "\\\\r\\\\n").replaceAll("\\r", "\\\\r").replaceAll("\\n", "\\\\n");
	}

	@Override
	public Class<?> getTargetClass() {
		return clazz;
	}

	@Override
	public void setTargetClass(final Class<?> targetClass) {
		clazz=targetClass;
	}

	@SuppressWarnings("unchecked")
	@Override
	public final JUnitTestUnitBlockInterface<? extends Member>[] getJUnitTestUnitBlocks() {
		final SortedSet<JUnitTestUnitBlock<? extends Member>> set = new TreeSet<JUnitTestUnitBlock<? extends Member>>();
		set.addAll(this.privateMethodsJUnitTestUnitBlocks);
		set.addAll(this.methodsJUnitTestUnitBlocks);
		set.addAll(this.constructorsJUnitTestUnitBlocks);
		return set.toArray((JUnitTestUnitBlock<? extends Member>[])Array.newInstance(JUnitTestUnitBlock.class, set.size()));
	}

	@Override
	public final String getTestUnitClassName() {
		return this.clazz.getName().replace('$', '.')+"JProbeTest";
	}

	@SuppressWarnings("unchecked")
	@Override
	public final JUnitTestUnitBlockInterface<Constructor<?>>[] getConstructorsJUnitTestUnitBlocks() {
		return this.constructorsJUnitTestUnitBlocks
		.toArray((JUnitTestUnitBlock<Constructor<?>>[])Array.newInstance(JUnitTestUnitBlock.class, this.constructorsJUnitTestUnitBlocks.size()));

	}

	@SuppressWarnings("unchecked")
	@Override
	public final JUnitTestUnitBlockInterface<Method>[] getMethodsJUnitTestUnitBlocks() {
		return this.methodsJUnitTestUnitBlocks
		.toArray((JUnitTestUnitBlock<Method>[])Array.newInstance(JUnitTestUnitBlock.class, this.methodsJUnitTestUnitBlocks.size()));

	}

	@SuppressWarnings("unchecked")
	@Override
	public final JUnitTestUnitBlockInterface<Method>[] getPrivateMethodsJUnitTestUnitBlocks() {
		return this.privateMethodsJUnitTestUnitBlocks
		.toArray((JUnitTestUnitBlock<Method>[])Array.newInstance(JUnitTestUnitBlock.class, this.privateMethodsJUnitTestUnitBlocks.size()));
	}

	@Override
	public final void resetCollections(){
		this.privateMethodsJUnitTestUnitBlocks.clear();
		this.methodsJUnitTestUnitBlocks.clear();
		this.constructorsJUnitTestUnitBlocks.clear();
	}

	@Override
	public void generateTestCase()throws IOException{
		final File file = new File(this.testFileName);
		file.getParentFile().mkdirs();
		final FileOutputStream fos = new FileOutputStream(file);
		final OutputStreamWriter osw = new OutputStreamWriter(fos);
		osw.write(this.toString());
		osw.close();
		fos.close();
		System.out.println("/-----------------------------------------------------------------------------------");
		System.out.println("/ wriiten to file : " + this.testFileName);
		System.out.println("/-----------------------------------------------------------------------------------");

	}
	@Override
	public String getTestFileName() {
		return testFileName;
	}

	private static Object[] array(final Object a){
		final Object[]obj=(Object[]) Array.newInstance(Object.class, Array.getLength(a));
		for(int i=0;i<obj.length;i++){
			obj[i]=Array.get(a, i);
		}
		return obj;
	}

	@Override
	public String toString() {
		final StringBuffer sb = new StringBuffer();
		sb.append("package "+ clazz.getPackage().getName()+";\n\n");
		sb.append("import org.junit.Test;\n");
		sb.append("import java.util.Arrays;\n");
		sb.append("import java.util.ArrayList;\n");
		sb.append("import java.util.List;\n");
		sb.append("import java.lang.reflect.Array;\n");
		sb.append("import static org.junit.Assert.assertTrue;\n");
		sb.append("import static org.junit.Assert.assertFalse;\n");
		sb.append("import static org.junit.Assert.fail;\n");
		sb.append("import static org.junit.Assert.assertEquals;\n");
		sb.append("import static org.junit.Assert.assertNotNull;\n");
		sb.append("import static org.junit.Assert.assertNull;\n");

		sb.append("\npublic class "+this.clazz.getSimpleName()+"JProbeTestCase"+"{\n");


		sb.append("\n	private static String deepToString(final Object array){");
		sb.append("\n		if(!array.getClass().isArray()){");
		sb.append("\n			throw new RuntimeException(array+\" is not an array\");");
		sb.append("\n		}");
		sb.append("\n		final List<Object> list = new ArrayList<Object>();");
		sb.append("\n		for(int i=0;i<Array.getLength(array);i++){");
		sb.append("\n			list.add( Array.get(array, i) );");
		sb.append("\n		}");
		sb.append("\n		return Arrays.deepToString(list.toArray()).replaceAll(\"\\r\\n\", \"\\\\r\\\\n\").replaceAll(\"\\r\", \"\\\\r\").replaceAll(\"\\n\", \"\\\\n\")  ;");
		sb.append("\n	}");
		
		for(final Object obj:this.privateMethodsJUnitTestUnitBlocks){sb.append(obj);}
		for(final Object obj:this.methodsJUnitTestUnitBlocks){sb.append(obj);}
		for(final Object obj:this.constructorsJUnitTestUnitBlocks){sb.append(obj);}
		
		sb.append("}");
		return sb.toString();
	}
}
